/* 
 * File:    Client for "Tiny World of Warcraft" (TWW)
 * Authors: Jonathan Kotker (ee122-bw) and Shaan Mulchandani (ee122-bv)
 */

#define MIN(a, b) ((a) > (b) ? (b) : (a))
#define MAX(a, b) ((a) < (b) ? (b) : (a))

#include "bufferList.h"
#include "bufferListNode.h"
#include "constants.h"
#include "duplicate.h"
#include "duplicateList.h"
#include "fileio.h"
#include "packetAssembler.h"
#include "packetContents.h"
#include "serverInfo.h"
#include "serverInfoList.h"
#include "stringUtils.h"
#include "tracker_output.h"
#include "UDP_Assembler.h"
#include "UDP_Contents.h"
#include "UDP_Parser.h"

#include <arpa/inet.h>  /* for inet_ntoa() */
#include <ctype.h>      /* for isalnum() */
#include <errno.h>      /* for perror() */
#include <netdb.h>      /* for getprotobyname() */
#include <netinet/in.h> /* for struct sockaddr_in */
#include <signal.h>     /* for signal() */
#include <stdio.h>      /* for fprintf() */
#include <stdint.h>     /* for uint16_t, uint32_t, ... */
#include <stdlib.h>     /* for exit() */
#include <string.h>     /* for memset(), strstr() */
#include <sys/select.h> /* for select() */
#include <sys/socket.h> /* for socket() */
#include <sys/types.h>  /* for socket() */
#include <time.h>       /* for nanosleep() */
#include <unistd.h>     /* for getopt(), read(), write(), ... */

void processClientPacket(UDP_Contents*, serverInfoList*,
        duplicateList*, int, struct sockaddr*, socklen_t);

/*
 * Determines the ID of the server that will store the information
 * of the player with the provided name.
 */
unsigned int hashPlayerName(char* playerName, serverInfoList* servers) {
    unsigned int hashVal;
    for (hashVal = 0; *playerName != 0; playerName++) {
        hashVal = *playerName + (31 * hashVal);
    }
    return hashVal % (servers->numServers);
};

/*
 * Tasks to be run before exiting the program. `servers' is the list
 * of current servers, `history' is the history of requests received,
 * and `status' is the reason the program is exiting.
 */
void done(serverInfoList* servers, duplicateList* history, int status) {
    clearServerInfoList(servers);
    clearDuplicateList(history);
    exit(status);
};

/*
 * Processes bytes received from the client into a packet.
 * Performs error-checking.
 */
void processBytes(char* msgBuffer, int msgLength,
        serverInfoList* servers, duplicateList* history, int udpListener,
        struct sockaddr* clientAddress, socklen_t clientAddressLength) {
    if (msgLength <= 0) {
        on_malformed_udp(1);
    } else if (msgBuffer[0] == STORAGE_LOCATION_REQUEST) {
        if (msgLength < 16) {
            on_malformed_udp(1);
        } else if (msgLength >= 16) {
            UDP_Contents udpPacketContents;
            parseUdpPacket(msgBuffer, &udpPacketContents);
            processClientPacket(&udpPacketContents, servers, history,
            udpListener, clientAddress, clientAddressLength);
        }
    } else if (msgBuffer[0] == SERVER_AREA_REQUEST) {
        if (msgLength < 8) {
            on_malformed_udp(1);
        } else if (msgLength >= 8) {
            UDP_Contents udpPacketContents;
            parseUdpPacket(msgBuffer, &udpPacketContents);
            processClientPacket(&udpPacketContents, servers, history,
            udpListener, clientAddress, clientAddressLength);
        }
    } else { // Incorrect message type.
        on_malformed_udp(2);
    }
};

/*
 * Processes a packet sent by a client.
 */
void processClientPacket(UDP_Contents* contentsReceived,
        serverInfoList* servers, duplicateList* history, int udpListener,
        struct sockaddr* clientAddress, socklen_t clientAddressLength) {
    if (contentsReceived->errorFlag == 1)
        on_malformed_udp(0);

    // Determine if this packet has been seen before;
    // if so, send the same response as before.
    duplicate checker;
    duplicate* result;

    checker.id = contentsReceived->id;
    memcpy(&checker.server_ip_add, clientAddress, sizeof(*clientAddress));

    if ((result = existsInList(history, &checker)) != NULL) {
        sendto(udpListener, result->stored_response, result->response_length,
                0, clientAddress, clientAddressLength);
        return;
    }

    switch (contentsReceived->messageType) {
        case STORAGE_LOCATION_REQUEST:
        {
            char arr[MAX_BUFFER_SIZE];
            int serverNum, length;
            serverInfo* selectedServer;
            duplicate* entry = (duplicate *) (malloc(sizeof (duplicate)));

            if (entry == NULL) {
                fprintf(stderr,
                        "!malloc: Cannot reserve space for history entry");
                done(servers, history, EXIT_FAILURE);
            }

            contentsReceived->messageType = STORAGE_LOCATION_RESPONSE;
            serverNum = hashPlayerName(contentsReceived->playerName, servers);
            selectedServer = accessServerInfo(servers, serverNum);
            contentsReceived->server_ip_add =
                    ntohl(inet_addr(selectedServer->serverIpAddress));
            contentsReceived->server_udp_port = selectedServer->serverUdpPort;

            length = assembleUdpPacket(arr, contentsReceived);
            sendto(udpListener, arr, length, 0, clientAddress,
                    clientAddressLength);

            entry->id = contentsReceived->id;
            entry->response_length = length;
            // entry->server_ip_add = contentsReceived->server_ip_add;
            memcpy(&entry->server_ip_add, clientAddress, sizeof(*clientAddress));
            memcpy(entry->stored_response, arr, MAX_BUFFER_SIZE);
            addDuplicateInfo(history, entry);

            break;
        }

        case SERVER_AREA_REQUEST:
        {
            char arr[MAX_BUFFER_SIZE];
            serverInfo* selectedServer;
            int length, i;
            duplicate* entry = (duplicate *) (malloc(sizeof (duplicate)));

            if (entry == NULL) {
                fprintf(stderr,
                        "!malloc: Cannot reserve space for history entry");
                done(servers, history, EXIT_FAILURE);
            }

            for (i = 0; i < servers->numServers; i++) {
                selectedServer = accessServerInfo(servers, i);
                if ((selectedServer->minX <= contentsReceived->xLoc)
                        && (contentsReceived->xLoc <= selectedServer->maxX)) {
                    contentsReceived->minX = selectedServer->minX;
                    contentsReceived->maxX = selectedServer->maxX;
                    contentsReceived->minY = 0;
                    contentsReceived->maxY = 99;
                    contentsReceived->server_ip_add =
                            ntohl(inet_addr(selectedServer->serverIpAddress));
                    contentsReceived->server_tcp_port =
                            selectedServer->serverTcpPort;
                }
            }
            contentsReceived->messageType = SERVER_AREA_RESPONSE;

            length = assembleUdpPacket(arr, contentsReceived);
            sendto(udpListener, arr, length, 0, clientAddress,
                    clientAddressLength);

            entry->id = contentsReceived->id;
            entry->response_length = length;
            // entry->server_ip_add = contentsReceived->server_ip_add;
            memcpy(&entry->server_ip_add, clientAddress, sizeof(*clientAddress));
            memcpy(entry->stored_response, arr, MAX_BUFFER_SIZE);
            addDuplicateInfo(history, entry);

            break;
        }
    }
};

int main(int argc, char** argv) {
    // Set up variables to be used throughout the execution of the program.
    serverInfoList servers;
    initServerInfoList(&servers);

    duplicateList history;
    initDuplicateList(&history);

    if (argc != 5 || (strcmp(argv[1], "-f") != 0) ||
            (strcmp(argv[3], "-p")) != 0) {
        // Incorrect format of command-line arguments.
        fprintf(stdout,
                "Expected command-line argument format: -f <Configuration File> -p <port> \n");
        done(&servers, &history, EXIT_FAILURE);
    }

    // Parse command-line arguments.
    char* configFileName = argv[2];
    int trackerPort = atoi(argv[4]);

    // Initialize tracker using configuration file.
    FILE* configFp;
    if ((configFp = fopen(configFileName, "r")) == NULL) {
        fprintf(stdout, "!fopen: Configuration file cannot be opened");
        done(&servers, &history, EXIT_FAILURE);
    }

    char configLine[MAX_CONFIG_LINE_LEN];
    // Read the configuration file line-by-line.
    while (fgets(configLine, MAX_CONFIG_LINE_LEN, configFp) != NULL) {
        serverInfo* newServerInfo = (serverInfo*) (malloc(sizeof (serverInfo)));
        if (newServerInfo == NULL) {
            fprintf(stdout,
                    "!malloc: Cannot allocate memory for server information.\n");
            done(&servers, &history, EXIT_FAILURE);
        }

        // Obtain server information from line in configuration file.
        char serverIpAddress[MAX_IP_ADDRESS_LEN + 1];
        sscanf(configLine, "%s %d %d",
                serverIpAddress,
                &(newServerInfo->serverTcpPort),
                &(newServerInfo->serverUdpPort));
        serverIpAddress[MAX_IP_ADDRESS_LEN] = '\0';
        strncpy(newServerInfo->serverIpAddress,
                serverIpAddress, MAX_IP_ADDRESS_LEN);

        addServerInfo(&servers, newServerInfo);
    }

    // Convert the server information list into an array, for efficient lookup.
    convertToArray(&servers);

    // Determine the bounds on `X' per server.
    int numServers = servers.numServers;
    int serverId;
    for (serverId = 0; serverId < numServers; serverId++) {
        serverInfo* currServer = servers.infoArray[serverId];

        currServer->minX = (99 / numServers) * serverId;
        if (serverId == (numServers - 1))
            currServer->maxX = 99;
        else
            currServer->maxX = ((99 / numServers) * (serverId + 1)) - 1;
    }

    // Get listener socket file descriptor.
    int listener = socket(PF_INET, SOCK_DGRAM, 0);
    if (listener < 0) {
        // Unable to create connection
        fprintf(stdout, "!socket: Cannot create socket for tracker. \n");
        done(&servers, &history, EXIT_FAILURE);
    }

    // Bind to provided port.
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof (sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = htonl(INADDR_ANY);
    sin.sin_port = htons(trackerPort);
    if (bind(listener, (struct sockaddr *) & sin, sizeof (sin)) < 0) {
        fprintf(stdout, "!bind: Cannot bind port in tracker. \n");
        done(&servers, &history, EXIT_FAILURE);
    }

    // Set up variables for infinite loop.
    char msgBuffer[MAX_TRACKER_MSG_LEN]; // Buffer for received messages
    int msgLength; // Length of message received
    struct sockaddr clientAddress; // Address from where data was received
    socklen_t clientAddressLength; // Length of clientAddress
    clientAddressLength = sizeof (clientAddress);

    // Start an infinite loop to receive datagrams
    while (1) {
        msgLength = recvfrom(listener, msgBuffer, MAX_TRACKER_MSG_LEN,
                0, &clientAddress, &clientAddressLength);

        processBytes(msgBuffer, msgLength, &servers, &history,
                listener, &clientAddress, clientAddressLength);
    }

    done(&servers, &history, EXIT_SUCCESS);
};
